import tkinter as tk  # 导入 tkinter 并设置别名 tk
import importlib.util
import os
import pandas as pd
import akshare as ak
from datetime import datetime, timedelta
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import mplfinance as mpf
from tkinter import ttk, filedialog, scrolledtext, messagebox
from PIL import Image, ImageTk
import subprocess
import signal
import numpy as np
import json  # 确保导入 json 库
import sys
import matplotlib.font_manager as fm
import matplotlib.dates as mdates
import matplotlib.units as munits
import tiptool
import pytz
from tkinter import ttk
import markdown
from tkhtmlview import HTMLLabel

VERSION = "1.0.0开源版"

# 新增导入 win32api 和 win32con
if sys.platform == "win32":
    try:
        import win32api
        import win32con
    except ImportError:
        print("无法导入 win32api 或 win32con 模块，请确保 pywin32 库已安装。")

# 配置文件路径
CONFIG_FILE = os.path.join(os.path.dirname(__file__), 'config', 'config.json')
# print(f"配置文件路径: {CONFIG_FILE}")

# # 设置 matplotlib 全局字体为黑体
plt.rcParams['font.family'] = 'SimHei'
# 解决负号显示问题
plt.rcParams['axes.unicode_minus'] = False


# # 应用全局样式
# plt.rcParams.update(my_style)

class QuantizationApp:
    def __init__(self, root):

        try:
            # print("开始加载配置文件...")
            self.config = self.load_config()
            # 打印配置文件的 JSON 内容
            # print("配置文件内容如下:")
            # print(json.dumps(self.config, ensure_ascii=False, indent=4))
            # print("配置文件加载成功:", self.config)
        except Exception as e:
            print(f"配置文件加载失败: {e}")
            self.config = {
                "start_date": "2020-01-01",
                "stock_code": "600000",
                "principal": 10000,
                "fee_rate": 0.0003,
                "stop_loss_rate": 0.1,
                "drawdown_rate": 0.05
            }

        # 设置窗口初始宽度，增加界面宽度
        root.geometry("1400x900")  # 可根据需要调整宽度和高度
        self.root = root
        self.root.title(f"丽敏量化{VERSION}")
        # 设置 matplotlib 支持中文
        font_names = ['Microsoft YaHei', 'SimHei', 'WenQuanYi Zen Hei', 'Noto Sans CJK SC']
        font_path = None
        for font_name in font_names:
            try:
                font_path = fm.findfont(fm.FontProperties(family=font_name))
                break
            except Exception:
                continue

        if font_path:
            # Manually specify the font file path, modify it according to the actual situation
            font_path = r'C:\Windows\Fonts\msyh.ttc'  # Take Microsoft YaHei as an example
            my_font = fm.FontProperties(fname=font_path)
            plt.rcParams['font.family'] = my_font.get_name()
            plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示问题
        else:
            print("未找到合适的中文字体，请检查系统字体")

        # Bind the signal handler
        if sys.platform == "win32" and 'win32api' in locals():
            win32api.SetConsoleCtrlHandler(self.on_close, True)
        else:
            signal.signal(signal.SIGINT, self.on_close)
        self.root.protocol("WM_DELETE_WINDOW", self.on_close)

        # 创建菜单栏
        menubar = tk.Menu(self.root)

        # 选股菜单
        stock_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="高级功能", menu=stock_menu)
        stock_menu.add_command(label="选股", command=self.show_coming_soon)
        stock_menu.add_command(label="批量回测", command=self.show_coming_soon)

        # # 设置菜单
        # setting_menu = tk.Menu(menubar, tearoff=0)
        # setting_menu.add_command(label="设置", command=self.show_coming_soon)
        # menubar.add_cascade(label="设置", menu=setting_menu)

        # 关于菜单
        about_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="关于", menu=about_menu)  # 添加这行代码，设置标签为“关于”
        about_menu.add_command(label="帮助", command=self.show_help)
        about_menu.add_command(label="许可", command=self.show_license)
        about_menu.add_command(label="版本", command=self.show_version)
        about_menu.add_command(label="赞助", command=self.show_donation)
        about_menu.add_command(label="联系", command=self.show_contact)

        self.root.config(menu=menubar)

        # 上部区域：Matplotlib 图表
        self.figure = mpf.figure(style='yahoo', figsize=(10, 4), dpi=100)
        gs = self.figure.add_gridspec(2, 1, height_ratios=[3, 1])
        self.ax = self.figure.add_subplot(gs[0, 0])
        self.ax_vol = self.figure.add_subplot(gs[1, 0], sharex=self.ax)  # K-line chart and volume chart height ratio is 3:1
        self.canvas = FigureCanvasTkAgg(self.figure, master=self.root)
        self.canvas.get_tk_widget().grid(row=1, column=0, columnspan=2, sticky="nsew", pady=5)

        # 配置根窗口的行和列权重，使图表区域能随窗口大小变化
        self.root.grid_rowconfigure(1, weight=1)  # 第 1 行（图表所在行）权重设为 1
        self.root.grid_columnconfigure(0, weight=1)  # 第 0 列权重设为 1
        self.root.grid_columnconfigure(1, weight=1)  # 第 1 列权重设为 1

        # 上部和中部之间的分割线，注意行号调整
        top_middle_separator = ttk.Separator(self.root, orient=tk.HORIZONTAL)
        top_middle_separator.grid(row=2, column=0, columnspan=2, sticky="ew", pady=5)

        # 中部区域（原下部区域），注意行号调整
        middle_frame = tk.Frame(self.root)
        middle_frame.grid(row=3, column=0, columnspan=2, sticky="nsew")
        self.root.grid_rowconfigure(3, weight=0)

        # 配置 middle_frame 的列权重，使左右区域宽度一致
        middle_frame.grid_columnconfigure(0, weight=1)  # 左区权重
        middle_frame.grid_columnconfigure(1, weight=0)  # 分割线权重
        middle_frame.grid_columnconfigure(2, weight=1)  # 右区权重

        
        # # 创建一个新的标签行框架，用于放置标签
        # label_row_frame = tk.Frame(middle_frame)
        # label_row_frame.grid(row=0, column=0, columnspan=3, sticky="ew")

        # # 添加量化策略标签
        # strategy_label = tk.Label(label_row_frame, text="量化策略")
        # strategy_label.grid(row=0, column=0, sticky="w", pady=(0, 5))

        # 中部左区：策略列表和按钮
        left_middle_frame = tk.Frame(middle_frame)
        left_middle_frame.grid(row=0, column=0, sticky="nsew",padx=1)
        # 配置左区列权重，确保内部组件能自适应
        # left_middle_frame.grid_columnconfigure(0, weight=1)
        # left_middle_frame.grid_rowconfigure(1, weight=1)


        # 添加量化策略标签
        strategy_label = tk.Label(left_middle_frame, text="量化策略")
        strategy_label.grid(row=0, column=0, sticky="ew", pady=(0, 5))  # 水平填充，下方留 5 像素间距
        # strategy_label.grid(row=0, column=0, sticky="ew", pady=(0, 5))
        

        
        self.strategy_json_path = os.path.join('config', 'strategy.json')
        self.strategy_info = self.load_strategy_info()
        self.strategy_listbox = tk.Listbox(left_middle_frame, width=35, height=10,exportselection=False)
        # self.strategy_listbox.config(state=tk.NORMAL)  # 确保列表框处于正常状态
        self.strategy_listbox.grid(row=1, column=0, sticky="nsew")  # 让列表框填充可用空间
        # self.strategy_listbox.lift()
        self.load_strategies()
        self.restore_selection_from_json()
        # 绑定策略列表框的选择事件
        self.strategy_listbox.bind("<<ListboxSelect>>", self.on_strategy_select)
        # strategy_label.grid(row=1, column=0, sticky="ew", pady=(0, 5))


        # 为策略列表框绑定事件


        # self.strategy_listbox.bind("<Motion>", self.on_listbox_motion)
        # print("已为 strategy_listbox 绑定 <Motion> 事件")  # 添加调试信息
        # self.strategy_listbox.bind("<Leave>", self.on_listbox_leave)
        # print("已为 strategy_listbox 绑定 <Leave> 事件")  # 添加调试信息

        tiptool.create_tiptool(self.strategy_listbox,self.strategy_info, delay=300)

        
        # self.tiptool = tiptool.ToolTip(self.strategy_listbox, delay=500, bg="lightyellow", fg="black")
        # # 默认选中第一个策略
        # if self.strategy_listbox.size() > 0:
        #     self.strategy_listbox.selection_set(0)

        # 按钮框架
        button_frame = tk.Frame(left_middle_frame)
        button_frame.grid(row=2, column=0, sticky="w", pady=5)  # 左对齐，上方留 5 像素间距
        open_button = tk.Button(button_frame, text="打开", command=self.open_selected_strategy)
        open_button.grid(row=2, column=0, padx=5)  # 按钮间留 5 像素间距
        new_button = tk.Button(button_frame, text="新建", command=self.create_new_strategy)
        new_button.grid(row=2, column=1, padx=5)  # 按钮间留 5 像素间距

        # 中部左右区域中间分割线
        middle_separator = ttk.Separator(middle_frame, orient=tk.VERTICAL)
        middle_separator.grid(row=0, column=1, sticky="ns", padx=1)

        # 中部右区：日期控件、输入框和按钮
        right_middle_frame = tk.Frame(middle_frame)
        right_middle_frame.grid(row=0, column=2, sticky="nsew",padx=1)
        # right_middle_frame.grid_columnconfigure(0, weight=1)

        right_middle_frame.grid_columnconfigure(0, weight=0)
        right_middle_frame.grid_columnconfigure(1, weight=0)
        right_middle_frame.grid_columnconfigure(2, weight=0)
        right_middle_frame.grid_columnconfigure(3, weight=0)
        right_middle_frame.grid_columnconfigure(4, weight=0)
        right_middle_frame.grid_columnconfigure(5, weight=0)
        row_num = 0


        # 添加股票设置标签
        stock_settings_label = tk.Label(right_middle_frame, text="股票设置")
        stock_settings_label.grid(row=row_num, column=3, columnspan=3, sticky="ew", pady=5)
        row_num += 1


        # 年月开始和结束日期控件放在一排
        # date_frame = tk.Frame(right_middle_frame)
        # date_frame.pack(side=tk.TOP, fill=tk.X,pady=0)

        # 年月开始日期控件
        # start_date_frame = tk.Frame(date_frame)
        # start_date_frame.pack(side=tk.LEFT, pady=0)
        # tk.Label(right_middle_frame, text="开始日期").grid(row=row_num, column=0, sticky="w")
        # 从配置文件获取开始日期
        tk.Label(right_middle_frame, text="开始日期").grid(row=row_num, column=0, sticky="w")
        # 从配置文件获取开始日期
        start_date = datetime.strptime(self.config["start_date"], "%Y-%m-%d")
        self.start_year = ttk.Combobox(right_middle_frame, values=list(range(2000, datetime.now().year + 1)), width=5)
        self.start_year.set(start_date.year)
        self.start_year.grid(row=row_num, column=1, sticky="w")
        self.start_month = ttk.Combobox(right_middle_frame, values=list(range(1, 13)), width=3)
        self.start_month.set(start_date.month)
        self.start_month.grid(row=row_num, column=2, sticky="w")

 

        # 年月结束日期控件
        tk.Label(right_middle_frame, text="结束日期").grid(row=row_num, column=3, sticky="w")
        self.end_year = ttk.Combobox(right_middle_frame, values=list(range(2000, datetime.now().year + 1)), width=5)
        self.end_year.set(datetime.now().year)
        self.end_year.grid(row=row_num, column=4, sticky="w")
        self.end_month = ttk.Combobox(right_middle_frame, values=list(range(1, 13)), width=3)
        self.end_month.set(datetime.now().month)
        self.end_month.grid(row=row_num, column=5, sticky="w")

        row_num += 1

        # 目标代码输入框及股票简称显示框
        tk.Label(right_middle_frame, text="股票代码").grid(row=row_num, column=0, sticky="w")
        self.stock_code_entry = tk.Entry(right_middle_frame,width=10)
        self.stock_code_entry.insert(0, self.config["stock_code"])
        self.stock_code_entry.grid(row=row_num, column=1, sticky="ew")

        # 绑定事件，当股票代码输入框内容变化时，获取股票简称
        self.stock_code_entry.bind("<KeyRelease>", self.get_stock_name)
        tk.Label(right_middle_frame, text="股票简称").grid(row=row_num, column=2, sticky="w")
        self.stock_name_label = tk.Entry(right_middle_frame, state='readonly')
        self.stock_name_label.grid(row=row_num, column=3, sticky="ew")
        # 初始化时获取一次股票简称
        self.get_stock_name()

        row_num += 1

        # 初始资金输入框
        tk.Label(right_middle_frame, text="初始资金").grid(row=row_num, column=0, sticky="w")
        self.cost_entry = tk.Entry(right_middle_frame,width=10)
        self.cost_entry.insert(0, str(self.config["principal"]))  # 从配置文件获取本金
        self.cost_entry.grid(row=row_num, column=1, sticky="ew")

        row_num += 1

        # 绑定事件，先检查选中状态
        # self.cost_entry.bind("<KeyRelease>", self.check_and_maintain_selection)

        # 手续费率输入框
        tk.Label(right_middle_frame, text="手续费率").grid(row=row_num, column=0, sticky="w")
        self.fee_rate_entry = tk.Entry(right_middle_frame,width=10)
        self.fee_rate_entry.insert(0, str(self.config["fee_rate"]))  # 从配置文件获取手续费率
        self.fee_rate_entry.grid(row=row_num, column=1, sticky="ew")

        row_num += 1

        # 止损率输入框
        tk.Label(right_middle_frame, text="止损率 (%)").grid(row=row_num, column=0, sticky="w")
        self.stop_loss_rate_entry = tk.Entry(right_middle_frame, width=10)
        self.stop_loss_rate_entry.insert(0, str(self.config["stop_loss_rate"] * 100))  # 从配置文件获取止损率
        self.stop_loss_rate_entry.grid(row=row_num, column=1, sticky="w")

        row_num += 1

        # 新增回撤率输入框
        tk.Label(right_middle_frame, text="回撤率 (%)").grid(row=row_num, column=0, sticky="w")
        self.drawdown_rate_entry = tk.Entry(right_middle_frame, width=10)
        self.drawdown_rate_entry.insert(0, str(self.config["drawdown_rate"] * 100))  # 从配置文件获取回撤率
        self.drawdown_rate_entry.grid(row=row_num, column=1, sticky="w")

        row_num += 1

        # 新增止盈率输入框
        tk.Label(right_middle_frame, text="止盈率 (%)").grid(row=row_num, column=0, sticky="w")
        self.take_profit_rate_entry = tk.Entry(right_middle_frame, width=10)
        self.take_profit_rate_entry.insert(0, str(self.config.get("take_profit_rate", 0) * 100))  # 从配置文件获取止盈率
        self.take_profit_rate_entry.grid(row=row_num, column=1, sticky="w")

        row_num += 1

        # 回测按钮
        backtest_button = tk.Button(right_middle_frame, text="回测", command=self.backtest,width=10,height=2)
        backtest_button.grid(row=row_num, column=0, columnspan=3, pady=10)

        # 中部和下部之间的分割线
        middle_bottom_separator = ttk.Separator(self.root, orient=tk.HORIZONTAL)
        middle_bottom_separator.grid(row=4, column=0, columnspan=2, sticky="ew", pady=5)

        # 下部区域（原中部区域）
        bottom_frame = tk.Frame(self.root)
        bottom_frame.grid(row=5, column=0, columnspan=2, sticky="nsew")
        
        # 确保下部区域能随窗口缩放
        self.root.grid_rowconfigure(5, weight=1)  # 降低下部区域的权重，减少其高度
        # self.root.grid_columnconfigure(0, weight=1)
        # self.root.grid_columnconfigure(1, weight=1)

        # 配置下部区域的列可缩放
        bottom_frame.grid_columnconfigure(0, weight=1)
        bottom_frame.grid_columnconfigure(1, weight=0)
        bottom_frame.grid_columnconfigure(2, weight=1)

        # 下部左区：回测结果文本框和保存按钮
        left_bottom_frame = tk.Frame(bottom_frame)
        left_bottom_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
        # 配置左下部区域的列和行可缩放

        left_bottom_frame.grid_columnconfigure(0, weight=1)
        left_bottom_frame.grid_rowconfigure(1, weight=1)

        # 添加回测结果标签
        tk.Label(left_bottom_frame, text="回测结果").grid(row=0, column=0, sticky="w")

        # 固定回测结果文本框高度，宽度随窗口缩放
        self.quant_result_text = scrolledtext.ScrolledText(left_bottom_frame, height=6)
        self.quant_result_text.grid(row=1, column=0, sticky="nsew")

        # 添加回测结果保存按钮
        save_quant_result_button = tk.Button(left_bottom_frame, text="保存", command=self.save_quant_result)
        save_quant_result_button.grid(row=2, column=0, pady=5, sticky="s")

        # 下部中间分割线
        middle_separator = ttk.Separator(bottom_frame, orient=tk.VERTICAL)
        # 确保列索引和��部中间分割线一致
        middle_separator.grid(row=0, column=1, sticky="ns", padx=5) 

        # 下部右区：交易详情文本框和保存按钮
        right_bottom_frame = tk.Frame(bottom_frame)
        right_bottom_frame.grid(row=0, column=2, sticky="nsew", padx=5, pady=5)
        right_bottom_frame.grid_columnconfigure(0, weight=1)
        right_bottom_frame.grid_rowconfigure(1, weight=1)

        # 添加交易详情标签
        tk.Label(right_bottom_frame, text="交易详情").grid(row=0, column=0, sticky="w")

        # 设置交易详情文本框高度
        self.trade_detail_text = scrolledtext.ScrolledText(right_bottom_frame, height=6)
        self.trade_detail_text.grid(row=1, column=0, sticky="nsew")

        save_button = tk.Button(right_bottom_frame, text="保存", command=self.save_trade_detail)
        save_button.grid(row=2, column=0, pady=5, sticky="s")

    def load_strategy_info(self):
        """加载 strategy.json 文件内容"""
        strategy_json_path = os.path.join(os.path.dirname(__file__), 'config', 'strategy.json')
        if os.path.exists(strategy_json_path):
            try:
                with open(strategy_json_path, 'r', encoding='utf-8') as f:
                    return json.load(f)
            except Exception as e:
                print(f"加载 strategy.json 文件失败: {e}")
        return {}


    # def on_listbox_motion(self, event):
    #     """显示气泡提示框"""
    #     print("on_listbox_motion 事件被触发")  # 确认事件是否触发
    #     if self.strategy_listbox.size() == 0:
    #         print("strategy_listbox 为空")
    #         self.tiptool.text = ""
    #         self.tiptool.leave(event)
    #         return
    #     index = self.strategy_listbox.nearest(event.y)
    #     file_name = self.strategy_listbox.get(index)
    #     print(file_name)
    #     info = self.strategy_info.get(file_name)
    #     if info:
    #         info = info[0]
    #         tiptool_text = (
    #             f"描述: {info.get('description', '')}\n"
    #             f"创建日期: {info.get('createDate', '')}\n"
    #             f"创建人: {info.get('createBy', '')}\n"
    #             f"版本: {info.get('version', '')}"
    #         )
    #         print(tiptool_text)
    #         self.tiptool.text = tiptool_text
    #         self.tiptool.enter(event)

    #     else:
    #         print("info 数据为空，不显示提示信息")  # 打印提示信息
    #         self.tiptool.text = ""
    #         self.tiptool.leave(event)
    #     """隐藏气泡提示框"""
    #     self.tiptool.leave(event)

    # def create_tiptool(widget, delay=500, **kwargs):
    # # Bug 修复：将实例化类名改为 ToolTip
    #     tiptool = tiptool(widget, delay=delay, **kwargs)
    #     return tiptool
    # def on_listbox_leave(self, event=None):
    #     self.hide_tip()


    # def check_selection_and_maintain(self, event=None):
    #     if self.strategy_listbox.size() > 0 and not self.strategy_listbox.curselection():
    #         self.strategy_listbox.selection_set(0)
    #     self.maintain_selection(event)

    
    # def _get_and_maintain_selection(self):
    #     selected_indices = self.strategy_listbox.curselection()
    #     if not selected_indices:
    #         print("未获取到选中的索引")
    #         return

    #     if self.strategy_listbox.size() == 0:
    #         print("策略列表框为空，无法恢复选中状态")
    #         return

    #     selected_index = selected_indices[0]
    #     print(f"当前选中的索引: {selected_index}")

    #     # 强制更新界面，确保界面状态同步
    #     self.root.update_idletasks()
    #     print("即将清除旧的选中状态")  # 添加日志
    #     # 先清除旧的选中状态
    #     self.strategy_listbox.selection_clear(0, tk.END)
    #     print("已清除旧的选中状态")

    #     # 适当延长延迟时间，避免与其他事件冲突
    #     self.root.after(500, self._restore_selection, selected_index)
    def load_strategy_info(self):
        if os.path.exists(self.strategy_json_path):
            with open(self.strategy_json_path, 'r', encoding='utf-8') as f:
                return json.load(f)
        return {}

    def save_strategy_info(self):
        os.makedirs(os.path.dirname(self.strategy_json_path), exist_ok=True)
        with open(self.strategy_json_path, 'w', encoding='utf-8') as f:
            json.dump(self.strategy_info, f, ensure_ascii=False, indent=4)

    def load_strategies(self):
        self.strategy_listbox.delete(0, tk.END)
        for file_name in self.strategy_info.keys():
            self.strategy_listbox.insert(tk.END, file_name)

    def restore_selection_from_json(self):
        self.strategy_listbox.selection_clear(0, tk.END)  # 先清除所有选中状态
        for index, file_name in enumerate(self.strategy_listbox.get(0, tk.END)):
            strategy_info_list = self.strategy_info.get(file_name)
            if strategy_info_list and isinstance(strategy_info_list, list):
                strategy_info = strategy_info_list[0]  # 取列表的第一个元素
                selected = strategy_info.get('selected', 0)
                if selected == 1:
                    self.strategy_listbox.selection_set(index)
                    self.strategy_listbox.see(index)
                    break

    def on_strategy_select(self, event):
        """处理策略列表框选择事件，更新 strategy.json 文件"""
        selected_index = self.strategy_listbox.curselection()
        if selected_index:
            selected_index = selected_index[0]
            selected_file_name = self.strategy_listbox.get(selected_index)
            # 更新 strategy.json
            for file_name, info_list in self.strategy_info.items():
                if info_list and isinstance(info_list, list):
                    info = info_list[0]
                    info['selected'] = 1 if file_name == selected_file_name else 0
            self.save_strategy_info()

    def maintain_selection_and_get_stock_name(self, event=None):
        """保持策略选中状态并获取股票简称"""
        self.maintain_selection()
        self.get_stock_name(event)

    def load_strategies(self):
        """加载 strategy 目录下的 py 文件列表"""
        if os.path.exists("strategy"):
            for file in os.listdir("strategy"):
                if file.endswith(".py"):
                    self.strategy_listbox.insert(tk.END, file)
                    # print(f"已加载策略文件")  # 添加日志输出
        else:
            print("strategy 目录不存在")

    def open_selected_strategy(self):
        """打开选中的策略文件"""
        selected_index = self.strategy_listbox.curselection()
        if selected_index:
            file_name = self.strategy_listbox.get(selected_index)
            file_path = os.path.join("strategy", file_name)
            subprocess.Popen(["notepad.exe", file_path])

    def create_new_strategy(self):
        """新建策略文件"""
        subprocess.Popen(["notepad.exe"])

    def load_config(self):
        """加载配置文件"""
        if os.path.exists(CONFIG_FILE):
            try:
                with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
                    return json.load(f)
            except json.JSONDecodeError as json_err:
                print(f"JSON 解析错误: {json_err}")
                print(f"配置文件内容:\n{open(CONFIG_FILE, 'r', encoding='utf-8').read()}")
            except Exception as e:
                print(f"加载配置文件失败: {e}")
        # 如果配置文件不存在或加载失败，使用默认值
        return {
            "start_date": "2020-01-01",
            "stock_code": "600000",
            "principal": 10000,
            "fee_rate": 0.0003,
            "stop_loss_rate": 0.1,
            "drawdown_rate": 0.05,
            "take_profit_rate": 0.3
        }

    def save_config(self):
        """保存配置文件"""
        try:
            with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
                json.dump(self.config, f, ensure_ascii=False, indent=4)
            # print("配置文件保存成功")
        except Exception as e:
            print(f"保存配置文件失败: {e}")

    def backtest(self):
        try:
            start_year = self.start_year.get()
            start_month = int(self.start_month.get())
            end_year = self.end_year.get()
            end_month = int(self.end_month.get())
            # 计算开始日期和结束日期
            start_date = f"{start_year}-{start_month:02d}-01"
            end_date = f"{end_year}-{int(end_month):02d}-{self._get_last_day_of_month(int(end_year), int(end_month))}"
            stock_code = self.stock_code_entry.get()
            principal = float(self.cost_entry.get())
            fee_rate = float(self.fee_rate_entry.get())
            stop_loss_rate = float(self.stop_loss_rate_entry.get()) / 100
            drawdown_rate = float(self.drawdown_rate_entry.get()) / 100
            take_profit_rate = float(self.take_profit_rate_entry.get()) / 100  # 获取止盈率


            # 更新 self.config 字典
            self.config["start_date"] = start_date
            self.config["stock_code"] = stock_code
            self.config["principal"] = principal
            self.config["fee_rate"] = fee_rate
            self.config["stop_loss_rate"] = stop_loss_rate
            self.config["drawdown_rate"] = drawdown_rate
            self.config["take_profit_rate"] = take_profit_rate  # 更新止盈率到配置


            # 使用 akshare 获取历史行情数据
            df = self.get_stock_data(stock_code, start_date, end_date)
            # print(f"backtest 中 get_stock_data 返回数据类型: {type(df).__name__}")

            if not isinstance(df, pd.DataFrame):
                messagebox.showerror("Error", "The data retrieved is not a DataFrame. Please check the data source.")
                return
            if df.empty:
                messagebox.showerror("错误", "未获取到有效的股票数据，请检查输入信息。")
                return

            # 获取选中的策略文件
            selected_index = self.strategy_listbox.curselection()
            if not selected_index:
                messagebox.showerror("错误", "请选择一个量化策略。")
                return
            file_name = self.strategy_listbox.get(selected_index)
            module_name = os.path.splitext(file_name)[0]  # 去掉 .py 扩展名

            # 导入策略模块

            strategy_path = os.path.abspath('strategy')
            if strategy_path not in sys.path:
                sys.path.append(strategy_path)

            try:
                # 动态导入模块
                strategy_module = importlib.import_module(module_name)
                # 重新加载模块（可选，用于开发阶段）
                importlib.reload(strategy_module)
            except ImportError:
                messagebox.showerror("错误", f"无法导入 {module_name} 模块，请检查模块文件是否存在。")
                return

            # 假设策略模块中都有一个名为 simple_ma_strategy 的函数
            if hasattr(strategy_module, 'simple_ma_strategy'):
                strategy_func = getattr(strategy_module, 'simple_ma_strategy')
            else:
                messagebox.showerror("错误", f"{module_name} 模块中未找到 simple_ma_strategy 函数。")
                return

            data, quant_result, trade_detail = strategy_func(
                df, stop_loss_rate=stop_loss_rate,
                cost=principal, fee_rate=fee_rate, drawdown_rate=drawdown_rate,take_profit_rate=take_profit_rate
            )

            # 绘制 K 线图和标记买卖点
            self.plot_stock_data(data)

            # 生成回测结果说明性文字
            current_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            quant_info_text = (                
                f"{self.stock_name_label.get()}{stock_code} 在 {current_datetime} 执行了量化策略{file_name} ，时间从{self.start_year.get()}年{self.start_month.get()}月到{self.end_year.get()}年{self.end_month.get()}月，"
                f"手续费率{fee_rate:.4f}，止损率{stop_loss_rate * 100:.2f}%，回撤率{drawdown_rate * 100:.2f}%，止盈率{take_profit_rate * 100:.2f}%，得到回测结果如下：\n"
            )
            quant_info_text2 = (                
                f"{self.stock_name_label.get()}{stock_code} 在 {current_datetime} 执行了量化策略{file_name} ，时间从{self.start_year.get()}年{self.start_month.get()}月到{self.end_year.get()}年{self.end_month.get()}月，"
                f"手续费率{fee_rate:.4f}，止损率{stop_loss_rate * 100:.2f}%，回撤率{drawdown_rate * 100:.2f}%，止盈率{take_profit_rate * 100:.2f}%，得到交易详情如下：\n"
            )

           
            # print(trade_detail)

            alert_text = ""
            # 解析 trade_detail 第一行数据
            trade_lines = trade_detail.strip().split('\n')
            if len(trade_lines) > 1:  # 跳过标题行
                first_trade_line = trade_lines[1]
                parts = [part.strip() for part in first_trade_line.split(',')]  # 对分割后的每个部分去除空格
                if len(parts) >= 2:
                    trade_date_str = parts[0]
                    trade_direction = parts[1]  # 提取交易方向
                    try:
                        trade_date = datetime.strptime(trade_date_str, '%Y-%m-%d').date()
                        seven_days_ago = (datetime.now() - timedelta(days=7)).date()
                        if trade_date > seven_days_ago:
                            if trade_direction == '买入':
                                alert_text = "注意：最近有买点！\n"
                            elif trade_direction == '卖出':
                                alert_text = "注意：最近有卖点！\n"
                            else:
                                alert_text = ""
                    except ValueError as ve:
                        print(f"日期解析失败，日期字符串: {trade_date_str}，错误信息: {ve}")  # 打印错误信息帮助调试

            # 显示回测结果
            self.quant_result_text.delete("1.0", tk.END)
            self.quant_result_text.insert(tk.END, quant_info_text)

            if alert_text:
                # 确保 'red' 标签已配置
                if 'red' not in self.quant_result_text.tag_names():
                    self.quant_result_text.tag_config('red', foreground='red')
                # 在回测结果说明性文字下方插入提示文字，设置为红色
                self.quant_result_text.insert(tk.END, alert_text, 'red')

            for key, value in quant_result.items():
                if isinstance(value, float):
                    self.quant_result_text.insert(tk.END, f"{key}: {value:.2f}{'%' if '率' in key else ''}\n")
                else:
                    self.quant_result_text.insert(tk.END, f"{key}: {value}\n")

            # 显示交易详情，将 quant_info_text2 与 trade_detail 拼接
            self.trade_detail_text.delete("1.0", tk.END)
            self.trade_detail_text.insert(tk.END, quant_info_text2 + trade_detail)

            # 保存配置
            self.save_config()  # 调用单独的保存配置方法

        except ValueError as ve:
            messagebox.showerror("错误", f"输入值格式错误: {str(ve)}")
        except Exception as e:
            messagebox.showerror("错误", f"回测失败: {str(e)}")

    def get_stock_data(self, stock_code, start_date, end_date):
        """
        使用 akshare 获取股票日级历史行情数据
        :param stock_code: 股票代码
        :param start_date: 开始日期，格式：YYYY-MM-DD
        :param end_date: 结束日期，格式：YYYY-MM-DD
        :return: 包含历史行情数据的 DataFrame
        """
        try:
            # 补全股票代码前缀
            if stock_code.startswith(('00', '30', '08')):
                stock_code = f"sz{stock_code}"
            elif stock_code.startswith(('60', '68')):
                stock_code = f"sh{stock_code}"
            else:
                messagebox.showerror("错误", "不支持的股票代码，请输入 A 股代码。")
                return pd.DataFrame()
            print(stock_code[2:],start_date,end_date)

            # 使用 akshare 获取数据
            df = ak.stock_zh_a_hist(
                symbol=stock_code[2:],
                period="daily",
                start_date=start_date.replace("-", ""),
                end_date=end_date.replace("-", ""),
                adjust="qfq"
            )
            
            if not df.empty:
                df.columns = ['日期', '目标代码', '开盘', '收盘', '最高', '最低', '成交量', '成交额', '振幅', '涨跌幅', '涨跌额', '换手率']
                df['日期'] = pd.to_datetime(df['日期'], format='%Y-%m-%d')
                #df.set_index('日期', inplace=True)
                #print(df)
            else:
                print(f"未获取到历史行情数据")
            # print(f"get_stock_data 返回数据类型: {type(df).__name__}")      
            return df
        except Exception as e:
            print(f"获取股票数据失败: {e}")
            # 确保异常时返回空 DataFrame
            return pd.DataFrame()

    #关闭程序        
    def on_close(self, *args):
        """Handle the close event"""
        try:
            # 尝试销毁所有子进程（如果有）
            # 这里可以添加更多资源清理操作
            if self.root:
                self.root.withdraw()
                self.root.destroy()
        except Exception as e:
            print(f"关闭程序时出错: {e}")
        
        try:
            if sys.platform == "win32" and 'win32api' in locals() and 'win32con' in locals():
                try:
                    win32api.GenerateConsoleCtrlEvent(win32con.CTRL_C_EVENT, 0)

                except ImportError:
                    print(f"Error terminating subprocesses on Windows: {e}")
            else:
                sys.exit(0)
        except Exception as e:
            print(f"关闭程序时出错: {e}")

    def save_quant_result(self):
        """保存回测结果到文件"""
        file_path = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text files", "*.txt")])
        if file_path:
            with open(file_path, "w", encoding="utf-8") as f:
                f.write(self.quant_result_text.get("1.0", tk.END))

    def save_trade_detail(self):
        """保存交易详情到文件"""
        file_path = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text files", "*.txt")])
        if file_path:
            with open(file_path, "w", encoding="utf-8") as f:
                f.write(self.trade_detail_text.get("1.0", tk.END))

    def show_coming_soon(self):
        """显示敬请期待提示框"""
        messagebox.showinfo("提示", "高级版功能")

    def show_help(self):
        """以 Markdown 样式显示 readme.md 文件内容"""
        help_window = tk.Toplevel(self.root)
        help_window.title("帮助文档")
        try:
            # 获取 readme.md 文件的绝对路径
            readme_path = os.path.join(os.path.dirname(__file__), 'readme.md')
            with open(readme_path, 'r', encoding='utf-8') as f:
                md_content = f.read()
            # 将 Markdown 内容转换为 HTML
            html_content = markdown.markdown(md_content)
            # 在新窗口中显示 HTML 内容
            html_label = HTMLLabel(help_window, html=html_content)
            html_label.pack(fill="both", expand=True)
            html_label.fit_height()
        except FileNotFoundError:
            tk.messagebox.showerror("错误", "未找到 readme.md 文件，请检查文件是否存在。")
        except Exception as e:
            tk.messagebox.showerror("错误", f"显示帮助文档时出错: {str(e)}")


    def show_version(self):
        """显示版本信息"""
        messagebox.showinfo("版本", f"版本：丽敏量化{VERSION}\n作者：大隐\n贡献者：")

    def show_license(self):
        """显示许可信息"""
        messagebox.showinfo("许可", "本软件遵循MIT授权，复制、修改、合并、出版发行、散布、再授权及贩售软件及软件的副本，在软件和软件的所有副本中都必须包含版权声明和许可声明。")

    def show_contact(self):
        """显示联系信息"""
        messagebox.showinfo("联系作者\n", "QQ：\n582902929")

    def show_donation(self):
        """显示联系信息"""
        messagebox.showinfo("赞助请联系\n", "赞助请联系\nQQ：582902929\n或\n582902929@qq.com")

    def get_stock_name(self, event=None):
        try:
            stock_code = self.stock_code_entry.get()
            if not stock_code:
                self._update_stock_name_label("请输入股票代码")
                return

            # 处理股票代码，不再补全 sh/sz 前缀
            processed_stock_code = self._process_stock_code(stock_code)
            if not processed_stock_code:
                self._update_stock_name_label("请输入有效的 6 位股票代码")
                return

            try:
                # 打印传入的股票代码
                # print(f"即将传入 ak.stock_individual_info_em 的股票代码: {processed_stock_code}")
                # 使用 ak.stock_individual_info_em 获取股票信息
                stock_info = ak.stock_individual_info_em(symbol=processed_stock_code)
                # 调用 _extract_stock_name 方法提取股票简称
                stock_name = self._extract_stock_name(stock_info)
                self._update_stock_name_label(stock_name)
            except Exception as e:
                print(f"使用 ak.stock_individual_info_em 获取股票简称失败: {e}")
                self._update_stock_name_label("获取失败")

        except Exception as e:
            print(f"获取股票简称失败: {e}")
            self._update_stock_name_label("获取失败")

    def _process_stock_code(self, stock_code):
        """处理股票代码，不补全 sh/sz 前缀，直接返回 6 位有效代码"""
        if len(stock_code) == 6:
            return stock_code
        return None

    def _update_stock_name_label(self, text):
        """更新股票简称标签内容"""
        self.stock_name_label.config(state='normal')
        self.stock_name_label.delete(0, tk.END)
        self.stock_name_label.insert(0, text)
        self.stock_name_label.config(state='readonly')

    def _extract_stock_name(self, stock_info):
        """从股票信息中提取股票简称"""
        if isinstance(stock_info, pd.DataFrame) and not stock_info.empty:
            stock_name_row = stock_info[stock_info['item'] == '股票简称']
            if not stock_name_row.empty:
                return stock_name_row['value'].values[0]
            return "未找到股票简称"
        return "获取股票信息失败"

    def _get_last_day_of_month(self, year, month):
        """
        获取指定年月的最后一天
        :param year: 年份
        :param month: 月份
        :return: 最后一天的日期
        """
        if month == 12:
            next_month = 1
            next_year = year + 1
        else:
            next_month = month + 1
            next_year = year
        last_day = (datetime(next_year, next_month, 1) - timedelta(days=1)).day
        return last_day

    def plot_stock_data(self, data):
        """
        绘制股票 K 线图并标记买卖点
        :param data: 包含股票数据和买卖点信息的 DataFrame
        """
        # 清空之前的图表
        mpl.rcParams.update(mpl.rcParamsDefault)
        plt.close('all')
        self.figure.clear()

        # 彻底清除 Figure 对象
        self.figure.clear()
        for ax in self.figure.axes:
            ax.clear()
        self.figure.delaxes(self.figure.axes[0]) if self.figure.axes else None

        try:
            gs = self.figure.add_gridspec(2, 1, height_ratios=[3, 1])  # K 线图和成交量图高度比为 3:1
            self.ax = self.figure.add_subplot(gs[0, 0])
            self.ax_vol = self.figure.add_subplot(gs[1, 0], sharex=self.ax)

            # 数据预处理
            if '日期' not in data.columns or data['日期'].isnull().any():
                messagebox.showerror("错误", "数据中缺少日期列或日期列存在空值，请检查数据。")
                return

            # data['日期'] = pd.to_datetime(data['日期'], format='%Y-%m-%d', errors='coerce')
            if data['日期'].isnull().any():
                messagebox.showerror("错误", "日期列存在无法解析的日期，请检查数据格式。")
                return
            
            # 确保日期数据是 naive 类型
            data['日期'] = pd.to_datetime(data['日期']).dt.tz_localize(None)
            # data.set_index('日期', inplace=True)

            try:
                kline_data = data[['日期','开盘', '收盘', '最高', '最低', '成交量']].copy()
                kline_data.columns = ['date','Open', 'Close', 'High', 'Low', 'Volume']
                kline_data.set_index('date', inplace=True)
                # print("日期索引类型:", type(kline_data.index))
                # print(kline_data)
            
            except Exception as e:
                print(f"日期转换失败: {e}")
                messagebox.showerror("错误", "日期转换失败，请检查数据中的日期格式。")
                return

            # 检查并处理 kline_data 索引重复问题
            if kline_data.index.has_duplicates:
                kline_data = kline_data[~kline_data.index.duplicated(keep='first')]
                print("检测到重复索引，已移除重复项。")

            # 检查 kline_data 是否为空
            if kline_data.empty:
                messagebox.showerror("错误", "K 线数据为空，无法绘图")
                return

            # 检查 signal 列是否存在
            if 'signal' not in data.columns:
                messagebox.showerror("错误", "数据中缺少 'signal' 列")
                return

            # 将 signal 列转换为买卖信号列
            data['买卖信号'] = ''
            data.loc[data['signal'] == 1, '买卖信号'] = '买入'
            data.loc[data['signal'] == -1, '买卖信号'] = '卖出'

            # 准备买卖点数据
            buy_signals = data[data['买卖信号'] == '买入'].copy()
            # print(buy_signals)
            sell_signals = data[data['买卖信号'] == '卖出'].copy()

            # 创建额外的绘图数据，即买卖点
            addplot = []

            # 处理买入点
            if not buy_signals.empty:
                buy_signals = buy_signals.set_index('日期')
                # 将 buy_signals 索引转换为 datetime 类型
                buy_signals.index = pd.to_datetime(buy_signals.index, format='%Y-%m-%d')
                # 使用 kline_data 的索引重新索引，缺失值用 NaN 填充
                buy_close = pd.Series(index=kline_data.index, data=np.nan)
                buy_close.loc[buy_signals.index] = buy_signals['收盘']
                if not buy_close.dropna().empty:
                    buy_markers = mpf.make_addplot(
                        buy_close,
                        type='scatter',
                        marker='^',
                        markersize=100,
                        color='g',
                        label='买入',
                        ax=self.ax
                    )
                    addplot.append(buy_markers)

            # 处理卖出点
            if not sell_signals.empty:
                sell_signals = sell_signals.set_index('日期')
                # 将 sell_signals 索引转换为 datetime 类型
                sell_signals.index = pd.to_datetime(sell_signals.index, format='%Y-%m-%d')
                # 使用 kline_data 的索引重新索引，缺失值用 NaN 填充
                sell_close = pd.Series(index=kline_data.index, data=np.nan)
                sell_close.loc[sell_signals.index] = sell_signals['收盘']
                if not sell_close.dropna().empty:
                    sell_markers = mpf.make_addplot(
                        sell_close,
                        type='scatter',
                        marker='v',
                        markersize=100,
                        color='r',
                        label='卖出',
                        ax=self.ax
                    )
                    addplot.append(sell_markers)
            
            # # # 为x轴设置日期格式化
            # date_format = mdates.DateFormatter('%Y-%m-%d')
            # self.ax.xaxis.set_major_formatter(date_format)
            # self.ax_vol.xaxis.set_major_formatter(date_format)  # 如果有成交量子图

            import matplotlib.font_manager as fm

            # fonts = fm.findSystemFonts()
            # for font in fonts:
            #     try:
            #         font_name = fm.FontProperties(fname=font).get_name()
            #         print(font_name)
            #     except Exception:
            #         pass


            my_style = mpf.make_mpf_style(base_mpf_style='yahoo')  # Correct parameter to set the base mplfinance style
                # rc={
                #     'font.family': 'SimHei',  # Specify a font that supports Chinese characters
                #     'axes.unicode_minus': False  # Solve the negative sign display issue
                # }


            # 绘制 K 线图，使用 mav 参数生成移动平均线
            plot_result = mpf.plot(
                kline_data,
                ax=self.ax,
                type='candle',
                style=my_style,
                mav=(5, 20),
                volume=self.ax_vol,
                # datetime_format='%Y-%m-%d',
                show_nontrading=False,
                addplot=addplot,
                returnfig=True,
                xlim=(kline_data.index.min(), kline_data.index.max())
            )

            # 设置 K 线图 Y 轴标签为中文“价格”
            self.ax.set_ylabel("价格",rotation=-90, labelpad=15)

            # 导入所需模块
            from matplotlib.dates import MonthLocator, DateFormatter

            # 定义日期格式化器，设置日期显示格式为 'YYYY-MM'

            # 为成交量图的 X 轴设置月份定位器和日期格式化器
            self.ax_vol.xaxis.set_major_locator(MonthLocator())
            self.ax_vol.xaxis.set_major_formatter(DateFormatter('%Y-%m'))

            # 显示成交量图 X 轴下部刻度标签，并旋转 45 度避免重叠
            self.ax_vol.tick_params(axis='x', bottom=True, labelbottom=True)

            # 生成标题
            title = f"{self.stock_name_label.get()}{self.stock_code_entry.get()} {self.start_year.get()}年{self.start_month.get()}月到{self.end_year.get()}年{self.end_month.get()}月回测"

            # 设置标题
            self.ax.set_title(title, fontsize=10)

            # 调整子图布局，让图表尽量充满绘图区域
            self.figure.subplots_adjust(top=0.95, left=0.1, right=0.95, bottom=0.1)

            # 禁用成交量图 Y 轴的科学计数法
            self.ax_vol.ticklabel_format(axis='y', style='plain')

            # 隐藏 K 线图的 X 轴下部标签
            self.ax.set_xticklabels([])
            self.ax.tick_params(axis='x', bottom=False, labelbottom=False)
        
            # # # 设置成交量图 X 轴日期格式
            # date_format = mdates.DateFormatter('%Y-%m-%d')
            # self.ax_vol.xaxis.set_major_formatter(date_format)

            # 显示成交量图的 X 轴下部
            self.ax_vol.tick_params(axis='x', bottom=True, labelbottom=True)

            # 将成交量图的 Y 轴刻度和标签移到右边
            self.ax_vol.yaxis.tick_right()
            self.ax_vol.yaxis.set_label_position("right")
            self.ax_vol.set_ylabel("成交量", rotation=-90, labelpad=15)  # 设置成交量名称在右边

            # # 自动调整 X 轴刻度，避免过于密集
            # self.ax_vol.xaxis.set_major_locator(mdates.AutoDateLocator())
            # self.ax_vol.tick_params(axis='x', rotation=45)

            # 隐藏 K 线图 Y 轴左边的刻度标签
            self.ax.tick_params(axis='y', labelleft=False, left=False, labelright=True, right=True)

            # 调整子图间距
            self.figure.subplots_adjust(hspace=0.1, top=1, left=0.1, right=1)
            self.figure.tight_layout()

            # 更新图表
            self.canvas.draw()

            # 手动设置均线标签并提取图例元素
            handles = []
            labels = []
            ma_periods = [5, 20]
            lines = self.ax.get_lines()

            # 提取买卖点图例元素
            handles = []
            labels = []
            from matplotlib.collections import PathCollection  # 导入 PathCollection

            for artist in self.ax.get_children():
                if isinstance(artist, (PathCollection, mpl.lines.Line2D)):
                    label = artist.get_label()
                    if label in ['买入', '卖出']:
                        if artist not in handles:
                            handles.append(artist)
                            labels.append(label)

            # 确保 lines 数量足够，添加均线图例
            lines = self.ax.get_lines()
            ma_periods = [5, 20]
            if len(lines) >= len(ma_periods):
                for i, period in enumerate(ma_periods):
                    line = lines[-len(ma_periods) + i]
                    line.set_label(f'MA{period}')
                    # 检查均线图例是否已经存在
                    if line not in handles:
                        handles.append(line)
                        labels.append(f'MA{period}')

            # 调试信息，打印提取的 handles 和 labels
            # print("提取的 handles:", handles)
            # print("提取的 labels:", labels)


            # 添加合并后的图例，将图例位置调整到左边
            if handles:
                self.ax.legend(
                    handles, 
                    labels, 
                    loc='upper left', 
                    bbox_to_anchor=(0, 1), 
                    borderaxespad=0., 
                    frameon=True,
                    handlelength=2,  # 增加图例标记的长度
                    handleheight=1.5,
                    ncol=1)   # 增加图例标记的高度
                    # labelspacing=0.8 # 增加图例项之间的垂直间距

            # 再次更新图表
            self.canvas.draw()

        except Exception as e:
            print(f"绘制 K 线图出错: {e}")
            messagebox.showerror("绘图错误", f"绘制 K 线图时出错: {str(e)}")
        
    
        # return fig, axes


# 添加 main 入口代码
if __name__ == "__main__":
    root = tk.Tk()  # 创建 tkinter 根窗口
    app = QuantizationApp(root)  # 实例化 QuantizationApp 类
    root.mainloop()  # 启动 tkinter 主事件循环

    try:
        app.root.mainloop()
    except KeyboardInterrupt:
        print("程序已被手动中断，正在退出...")
        # 执行清理操作，如关闭窗口、释放资源等
        if hasattr(app.root, 'destroy'):
            app.root.destroy()